package game;

import made.ann.ANNPacMan;
import made.ann.NeuralNetwork;
import made.ga.GAPacMan;
import made.ga.GeneticAlgorithm;
import made.ql.QLPacMan;
import made.util.Config;
import made.util.GameStateRecorder;
import made.util.Logger;
import made.util.Config.DEBUG_LEVEL;
import made.util.Config.MODE;
import game.controllers.Human;
import game.controllers.PacManController;
import game.controllers.GhostController;
import game.controllers.examples.AttractRepelGhosts;
import game.controllers.examples.Legacy;
import game.controllers.examples.Legacy2TheReckoning;
import game.controllers.examples.NearestPillPacMan;
import game.controllers.examples.NearestPillPacManVS;
import game.controllers.examples.RandomGhosts;
import game.controllers.examples.RandomNonRevPacMan;
import game.controllers.examples.RandomPacMan;
import game.core.G;
import game.core.GameView;
import game.core._G_;
import game.core.Replay;
import game.core._RG_;
import game.entries.pacman.*;
import game.entries.ghosts.*;
import ann.Data;

/*
 * This class may be used to execute the game in timed or un-timed modes, with or without
 * visuals. Competitors should implement their controllers in game.entries.ghosts and 
 * game.entries.pacman respectively. The skeleton classes are already provided. The package
 * structure should not be changed (although you may create sub-packages in these packages).
 */
@SuppressWarnings("unused")
public class Exec
{
    // Several options are listed - simply remove comments to use the option you
    // want
    public static void main(String[] args)
    {
        Exec exec = new Exec();
//        StarterGhosts ghosts = new StarterGhosts();
//         Legacy ghosts = new Legacy();
         Legacy2TheReckoning ghosts = new Legacy2TheReckoning();
        // AttractRepelGhosts ghosts = new AttractRepelGhosts(true);
//         RandomGhosts ghosts = new RandomGhosts();

        // exec.runExperiment(new StarterPacMan(), ghosts, 500);

        // //////////////////////////////////////
        // ////////////////////////////// ANN TEST
        // //////////////////////////////////////
//        ANNPacMan pacMan = new ANNPacMan();
//         GAPacMan pacMan = new GAPacMan();
//         QLPacMan pacMan = new QLPacMan();

        // String name = "ga";
        // int neuronsPerLayer[] = { 3, 3, 3 };
        //
        // // Create neural network
        // NeuralNetwork net = new NeuralNetwork(neuronsPerLayer, name, 0.3f,
        // 0.9f, 100000);
        // net.train();
        // Logger.logLine("Testing trained neural network\n");
        // net.testOnData();

//         exec.runGameTimed(pacMan, ghosts, true);
//          exec.runExperiment(pacMan, ghosts, 500);
//          exec.runExperiment(pacMan, ghosts, 500);

        // //////////////////////////////////////
        // ////////////////////////////// GA TEST
        // //////////////////////////////////////
        // 24 8 87

        // GAPacMan pacMan = new GAPacMan();
        // GeneticAlgorithm ga = new GeneticAlgorithm("PacManController",
        // Config.CHROMOSOME_SIZE, Config.POPULATION_SIZE);
        // int gamesPerChromosome =20;
        // int generations = 0;
        // float currentAverageFitness = 0.0f;
        // int averageScore = 0;
        //
        //
        // while (true)
        // {
        // if (Config.MPM_DEBUG_LEVEL == DEBUG_LEVEL.VERBOSE)
        // {
        // Logger.logMethod();
        // Logger.log("Evaluating generation " + generations);
        // }
        //
        // if (Config.MPM_DEBUG_LEVEL == DEBUG_LEVEL.SILENT && Config.MPM_MODE
        // == MODE.TRAIN)
        // Logger.log("" + generations);
        //
        // for (int i = 0; i < Config.POPULATION_SIZE; i++)
        // {
        // pacMan.init();
        // pacMan.setPredicateVariables(ga.getChromosome(i).getPhenotype());
        // int averageChromosomeFitness = 0;
        //
        // // Logger.log(".");
        //
        // for (int j = 0; j < gamesPerChromosome; j++)
        // {
        // exec.runExperiment(pacMan, ghosts, 1);
        // averageChromosomeFitness += pacMan.getFitness();
        // averageScore += pacMan.getScore();
        // }
        //
        // averageChromosomeFitness = (int) (averageChromosomeFitness / (float)
        // gamesPerChromosome);
        // ga.setChromosomeFitness(i, averageChromosomeFitness);
        // }
        //
        // averageScore = averageScore / (Config.POPULATION_SIZE *
        // gamesPerChromosome);
        //
        // if (Config.MPM_DEBUG_LEVEL == DEBUG_LEVEL.VERBOSE)
        // Logger.logLine("Average score for generation is: " + averageScore);
        //
        // ga.evaluateGeneration(generations);
        //
        // // if (ga.hasConverged())
        // // break;
        //
        // if (ga.hasConverged())
        // break;
        //
        // ga.produceNextGeneration();
        //
        //
        //
        // generations++;
        // }

        // int games = 0;
        //
        // while (!pacMan.completeTrainingSetSaved())
        // {
        // Logger.logLine("" + games);
        // pacMan.init();
        // // exec.runGameTimed(pacMan, ghosts, true);
        // exec.runExperiment(pacMan, ghosts, 1);
        // pacMan.printSampleTypeStats();
        // games++;
        // try
        // {
        // Thread.sleep(2000);
        // } catch (InterruptedException e)
        // {
        // // TODO Auto-generated catch block
        // e.printStackTrace();
        // }
        // }

        // Config

        // //////////////////////////////////////
        // ////////////////////////////// QL TEST
        // //////////////////////////////////////

        // 18 7 80
         QLPacMan pacMan = new QLPacMan();

        // Training

         int trials = 0;
         int accumulatedScore = 0;
         int averageScore = 0;
        
         Logger.logLine("Q learning start");
         boolean done = false;
         // while (pacMan.getScore() < 10000)
         // while (trials < 10000)
         while (!done)
         {
         accumulatedScore += pacMan.getScore();
        
         pacMan.init();
         // exec.runGameTimed(pacMan, ghosts, true);
         exec.runExperiment(pacMan, ghosts, 1);
         trials++;
        
         if (trials % 100 == 0)
         {
         Logger.log("" + trials);
         pacMan.printDeltaQAverage();
         // Logger.logLine("Average score over 100 games: " + averageScore);
         Logger.log(" " + pacMan.getScore());
         // pacMan.printTable();
         accumulatedScore = 0;
         Logger.log("\n");
         }
        
         done = pacMan.qLearningDone();
        
         if (done)
         {
         Logger.log("" + trials);
         pacMan.printDeltaQAverage();
         Logger.log(" " + pacMan.getScore());
         }
         }
        
         Logger.logLine("Q learning done after " + trials + " games");
        
        // Config

        // exec.runGameTimed(pacMan, ghosts, true);
        // exec.runExperiment(pacMan, ghosts, 500);
    }

    protected int pacDir;
    protected int[] ghostDirs;
    protected _G_ game;
    protected PacMan pacMan;
    protected Ghosts ghosts;
    protected boolean pacmanPlayed, ghostsPlayed;

    /*
     * For running multiple games without visuals. This is useful to get a good
     * idea of how well a controller plays against a chosen opponent: the random
     * nature of the game means that performance can vary from game to game.
     * Running many games and looking at the average score (and standard
     * deviation/error) helps to get a better idea of how well the controller is
     * likely to do in the competition.
     */
    public void runExperiment(PacManController pacManController, GhostController ghostController, int trials)
    {
        double avgScore = 0;

        game = new _G_();

        for (int i = 0; i < trials; i++)
        {
            game.newGame();

            while (!game.gameOver())
            {
                long due = System.currentTimeMillis() + G.DELAY;
                game.advanceGame(pacManController.getAction(game.copy(), due), ghostController.getActions(game.copy(), due));
            }

            avgScore += game.getScore();
//            System.out.println(game.getScore());
        }

//        System.out.println(avgScore / trials);
    }

    /*
     * Run game without time limit. Very good for testing as game progresses as
     * soon as the controllers return their action(s). Can be played with and
     * without visual display of game states. The delay is purely for visual
     * purposes (as otherwise the game could be too fast if controllers compute
     * quickly. For testing, this can be set to 0 for fasted game play.
     */
    public void runGame(PacManController pacManController, GhostController ghostController, boolean visual, int delay)
    {
        game = new _G_();
        game.newGame();

        GameView gv = null;

        if (visual)
            gv = new GameView(game).showGame();

        while (!game.gameOver())
        {
            long due = System.currentTimeMillis() + G.DELAY;
            game.advanceGame(pacManController.getAction(game.copy(), due), ghostController.getActions(game.copy(), due));

            try
            {
                Thread.sleep(delay);
            } catch (Exception e)
            {
            }

            if (visual)
                gv.repaint();
        }
    }

    /*
     * Run game with time limit. This is how it will be done in the competition.
     * Can be played with and without visual display of game states.
     */
    public void runGameTimed(PacManController pacManController, GhostController ghostController, boolean visual)
    {
        game = new _G_();
        game.newGame();
        pacMan = new PacMan(pacManController);
        ghosts = new Ghosts(ghostController);

        GameView gv = null;

        if (visual)
        {
            gv = new GameView(game).showGame();

            if (pacManController instanceof Human)
                gv.getFrame().addKeyListener((Human) pacManController);
        }

        while (!game.gameOver())
        {
            pacMan.alert();
            ghosts.alert();

            try
            {
                Thread.sleep(G.DELAY);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }

            game.advanceGame(pacDir, ghostDirs);

            if (visual)
                gv.repaint();
        }

        pacMan.kill();
        ghosts.kill();
    }

    /*
     * Runs a game and records all directions taken by all controllers - the
     * data may then be used to replay any game saved using replayGame(-).
     */
    public void runGameTimedAndRecorded(PacManController pacManController, GhostController ghostController, boolean visual, String fileName)
    {
        StringBuilder history = new StringBuilder();
        int lastLevel = 0;
        boolean firstWrite = false; // this makes sure the content of any
                                    // existing files is overwritten

        game = new _G_();
        game.newGame();
        pacMan = new PacMan(pacManController);
        ghosts = new Ghosts(ghostController);

        GameView gv = null;

        if (visual)
        {
            gv = new GameView(game).showGame();

            if (pacManController instanceof Human)
                gv.getFrame().addKeyListener((Human) pacManController);
        }

        while (!game.gameOver())
        {
            pacMan.alert();
            ghosts.alert();

            try
            {
                Thread.sleep(G.DELAY);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }

            int[] actionsTaken = game.advanceGame(pacDir, ghostDirs);

            if (visual)
                gv.repaint();

            history = addActionsToString(history, actionsTaken, game.getCurLevel() == lastLevel);

            // saves actions after every level
            if (game.getCurLevel() != lastLevel)
            {
                Replay.saveActions(history.toString(), fileName, firstWrite);
                lastLevel = game.getCurLevel();
                firstWrite = true;
                history = new StringBuilder();
            }
        }

        // save the final actions
        Replay.saveActions(history.toString(), fileName, firstWrite);

        pacMan.kill();
        ghosts.kill();
    }

    /*
     * This is used to replay a recorded game. The controllers are given by the
     * class Replay which may also be used to load the actions from file.
     */
    public void replayGame(String fileName)
    {
        _RG_ game = new _RG_();
        game.newGame();

        Replay replay = new Replay(fileName);
        PacManController pacManController = replay.getPacMan();
        GhostController ghostController = replay.getGhosts();

        GameView gv = new GameView(game).showGame();

        while (!game.gameOver())
        {
            game.advanceGame(pacManController.getAction(game.copy(), 0), ghostController.getActions(game.copy(), 0));

            gv.repaint();

            try
            {
                Thread.sleep(G.DELAY);
            } catch (Exception e)
            {
            }
        }
    }

    private StringBuilder addActionsToString(StringBuilder history, int[] actionsTaken, boolean newLine)
    {
        history.append((game.getTotalTime() - 1) + "\t" + actionsTaken[0] + "\t");

        for (int i = 0; i < G.NUM_GHOSTS; i++)
            history.append(actionsTaken[i + 1] + "\t");

        if (newLine)
            history.append("\n");

        return history;
    }

    // sets the latest direction to take for each game step (if controller
    // replies in time)
    public void setGhostDirs(int[] ghostDirs)
    {
        this.ghostDirs = ghostDirs;
        this.ghostsPlayed = true;
    }

    // sets the latest direction to take for each game step (if controller
    // replies in time)
    public void setPacDir(int pacDir)
    {
        this.pacDir = pacDir;
        this.pacmanPlayed = true;
    }

    /*
     * Wraps the controller in a thread for the timed execution. This class then
     * updates the directions for Exec to parse to the game.
     */
    public class PacMan extends Thread
    {
        private PacManController pacMan;
        private boolean alive;

        public PacMan(PacManController pacMan)
        {
            this.pacMan = pacMan;
            alive = true;
            start();
        }

        public synchronized void kill()
        {
            alive = false;
            notify();
        }

        public synchronized void alert()
        {
            notify();
        }

        public synchronized void run()
        {
            while (alive)
            {
                try
                {
                    synchronized (this)
                    {
                        wait();
                    }

                    setPacDir(pacMan.getAction(game.copy(), System.currentTimeMillis() + G.DELAY));
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }

    /*
     * Wraps the controller in a thread for the timed execution. This class then
     * updates the directions for Exec to parse to the game.
     */
    public class Ghosts extends Thread
    {
        private GhostController ghosts;
        private boolean alive;

        public Ghosts(GhostController ghosts)
        {
            this.ghosts = ghosts;
            alive = true;
            start();
        }

        public synchronized void kill()
        {
            alive = false;
            notify();
        }

        public synchronized void alert()
        {
            notify();
        }

        public synchronized void run()
        {
            while (alive)
            {
                try
                {
                    synchronized (this)
                    {
                        wait();
                    }

                    setGhostDirs(ghosts.getActions(game.copy(), System.currentTimeMillis() + G.DELAY));
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
}